# 用于处理Nginx代理

import os
import shutil
import crossplane
import json
import logging
from copy import deepcopy
import tempfile
from ..configs import get_config, get_accessible_ports
from ..utils.utils import is_valid_docker_container_name
from ..utils.utils import get_free_port, get_free_port_in_allow_set

logger = logging.getLogger(__name__)


def find_node_by_key(data, target_key):
    """
    Recursively find a node in a nested list of dictionaries by key.

    :param data: The nested list of dictionaries to search.
    :param target_key: The key to search for.
    :return: The dictionary containing the target key, or None if not found.
    """
    if isinstance(data, dict):
        if target_key in data:
            return data
        for key, value in data.items():
            result = find_node_by_key(value, target_key)
            if result is not None:
                return result
    elif isinstance(data, list):
        for item in data:
            result = find_node_by_key(item, target_key)
            if result is not None:
                return result
    return None


def find_nodes_by_key_value(data, target_key, target_value):
    """
    Recursively find all nodes in a nested list of dictionaries by key and value.

    :param data: The nested list of dictionaries to search.
    :param target_key: The key to search for.
    :param target_value: The value associated with the key to search for.
    :return: A list of dictionaries containing the target key and value.
    """
    results = []
    if isinstance(data, dict):
        if target_key in data and data[target_key] == target_value:
            results.append(data)
        for key, value in data.items():
            results.extend(find_nodes_by_key_value(value, target_key, target_value))
    elif isinstance(data, list):
        for item in data:
            results.extend(find_nodes_by_key_value(item, target_key, target_value))
    return results


def read_nginx_config(file_path, ignore_failed=False):
    """
    Read and parse an Nginx configuration file.

    :param file_path: The path to the Nginx configuration file.
    :return: The parsed content of the configuration file.
    """
    payload = crossplane.parse(file_path)
    if payload["status"] == "failed" and not ignore_failed:
        raise ValueError(f"Failed to parse {file_path}: {payload}")
    return payload["config"][0]["parsed"]


def read_server_config(server_config_file_path: str):
    """
    Read the nginx server config from one config file.

    We manually add ``http {}`` to the config file, so that we can use ``crossplane`` to parse it.
    """
    with open(server_config_file_path, "r") as f:
        server_config_file_content = f.read()
    # Create a named tempfile to store the modified config
    with tempfile.NamedTemporaryFile(delete=False) as temp_file:
        temp_file.write(f"http {{\n{server_config_file_content}\n}}".encode())
        temp_file.flush()
        temp_file.seek(0)
        # print(temp_file.read())
        temp_file.seek(0)
        temp_file_path = temp_file.name
        parsed_content = crossplane.parse(temp_file_path)

        # print(parsed_content)
        # if parsed_content["status"] == "failed":
        #     raise ValueError(f"Failed to parse {server_config_file_path}: {server_config_file_content}")
        return parsed_content["config"][0]["parsed"]


def write_server_config(file_path, config_content):
    """
    Write the updated Nginx configuration content to a file.

    :param file_path: The path to the Nginx configuration file.
    :param config_content: The updated configuration content.
    """
    s = crossplane.build(config_content)
    assert s.startswith("http {")
    s = s.removeprefix("http {")
    assert s.endswith("}")
    s = s.removesuffix("}")
    with open(file_path, "w") as f:
        f.write(s)


def write_nginx_config(file_path, config_content):
    """
    Write the updated Nginx configuration content to a file.

    :param file_path: The path to the Nginx configuration file.
    :param config_content: The updated configuration content.
    """
    with open(file_path, "w") as f:
        f.write(crossplane.build(config_content))


def update_additional_http_servers(additional_server_conf_path: str, with_ssl: bool):
    """
    Update the ssl configuration of all additional http servers over isolated ports
      based on the configuration.
    """
    config = read_server_config(additional_server_conf_path)
    for server_node in find_nodes_by_key_value(config, "directive", "server"):
        modify_http_server_config(server_node, with_ssl)
    write_server_config(additional_server_conf_path, config)


def modify_http_server_config(server_node: dict, with_ssl: bool, port: int = -1):
    # server_node = find_nodes_by_key_value(nginx_config, "directive", "server")[0]
    listen_node = find_nodes_by_key_value(server_node, "directive", "listen")[0]
    assert len(listen_node["args"]) > 0
    port = port if port >= 0 else int(listen_node["args"][0])
    listen_node["args"] = [str(port)]

    if with_ssl:
        listen_node["args"].append("ssl")

    # 修改SSL
    if with_ssl:
        logger.info("enabling ssl...")
        ssl_certificate_nodes = find_nodes_by_key_value(
            server_node, "directive", "ssl_certificate"
        )
        if len(ssl_certificate_nodes) == 0:
            server_node["block"].append(
                {
                    "directive": "ssl_certificate",
                    "args": ["/cert/cert.pem"],
                }
            )

        ssl_certificate_keys = find_nodes_by_key_value(
            server_node, "directive", "ssl_certificate_key"
        )
        if len(ssl_certificate_keys) == 0:
            ssl_node = {"directive": "ssl_certificate_key", "args": ["/cert/cert.key"]}
            server_node["block"].append(ssl_node)

        ssl_protocol_nodes = find_nodes_by_key_value(
            server_node, "directive", "ssl_protocols"
        )

        if len(ssl_protocol_nodes) == 0:
            ssl_protocol_node = {
                "directive": "ssl_protocols",
                "args": ["TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"],
            }
            server_node["block"].append(ssl_protocol_node)


def update_nginx_config(nginx_conf_file: str, port: int, with_ssl: bool):
    """
    Update the listening port of Nginx based on the configuration.
    """
    # config = get_config()
    nginx_config = read_nginx_config(nginx_conf_file)
    server_node = find_nodes_by_key_value(nginx_config, "directive", "server")[0]
    modify_http_server_config(server_node, with_ssl, port)
    write_nginx_config(nginx_conf_file, nginx_config)


def format_nginx_url_prefix():
    """
    Format the editor URL based on the configuration.
    Note that the returned url is not end with '/'
    """
    config = get_config()
    if config["nginx_ssl_used"]:
        return f"https://{config['nginx_domain_name']}:{config['nginx_listen_port']}"
    else:
        domain_name = (
            config.get("nginx_domain_name")
            or config["nginx_ip"]
            or "${window.location.hostname}"
        )
        return f"http://{domain_name}:{config['nginx_listen_port']}"


def add_nginx_location(template_path, target_path, new_location, new_proxy_pass):
    """
    Add a new location to an Nginx configuration file based on a template.

    :param template_path: The path to the template Nginx configuration file.
    :param target_path: The path to the target Nginx configuration file.
    :param new_location: The new location path.
    :param new_proxy_pass: The new proxy_pass URL.
    """
    # Read the template and target configuration files
    template_content = read_nginx_config(template_path, ignore_failed=True)
    target_content = read_nginx_config(target_path)

    # Find the first location block in the template
    original_location_dict = next(
        filter(
            lambda _location_node_dict: _location_node_dict["args"][0]
            == "/proxy-demo/",
            find_nodes_by_key_value(template_content, "directive", "location"),
        )
    )
    location_dict = deepcopy(original_location_dict)

    # Modify the location and proxy_pass fields
    location_dict["args"][0] = new_location
    proxy_pass_node = find_nodes_by_key_value(location_dict, "directive", "proxy_pass")[
        0
    ]
    proxy_pass_node["args"][0] = new_proxy_pass

    # Find the server block in the target configuration and append the new location
    directive_server = find_nodes_by_key_value(target_content, "directive", "server")[0]

    # 将block中的元素进行去重
    directive_server_blocks = []
    for item in directive_server["block"]:
        if not (item["directive"] == "location" and item["args"][0] == new_location):
            directive_server_blocks.append(item)

    directive_server_blocks.append(location_dict)
    directive_server["block"] = directive_server_blocks

    # Write the updated configuration back to the target file
    write_nginx_config(target_path, target_content)


def add_http_server(template_path, target_path, port: int, new_proxy_pass):
    """
    为现有的nginx文件添加一个http_server。
    """
    # 读取模板和目标配置文件
    template_content = read_server_config(template_path)
    target_content = read_server_config(target_path)

    # 找到模板中的 server 块
    server_blocks = find_nodes_by_key_value(template_content, "directive", "server")
    if not server_blocks:
        raise ValueError(f"No server block found in template file: {template_path}")

    # 修改 server 块中的 proxy_pass
    for server_block in server_blocks:
        proxy_pass_nodes = find_nodes_by_key_value(
            server_block, "directive", "proxy_pass"
        )
        if proxy_pass_nodes:
            proxy_pass_nodes[0]["args"][0] = new_proxy_pass
        modify_http_server_config(server_block, get_config()["nginx_ssl_used"])

    # 找到目标文件中的 http 块
    http_block = find_nodes_by_key_value(target_content, "directive", "http")[0]

    # 检查是否已经存在具有相同 proxy_pass 的 server 块
    existing_server_blocks = find_nodes_by_key_value(http_block, "directive", "server")
    existing_proxy_passes = set()
    for existing_server_block in existing_server_blocks:
        existing_proxy_pass_nodes = find_nodes_by_key_value(
            existing_server_block, "directive", "proxy_pass"
        )
        if existing_proxy_pass_nodes:
            existing_proxy_pass = existing_proxy_pass_nodes[0]["args"][0]
            existing_proxy_passes.add(existing_proxy_pass)

    # 将 server 块插入到 http 块中（如果不存在相同的 proxy_pass）
    for server_block in server_blocks:
        proxy_pass_nodes = find_nodes_by_key_value(
            server_block, "directive", "proxy_pass"
        )
        if proxy_pass_nodes:
            proxy_pass = proxy_pass_nodes[0]["args"][0]
            if proxy_pass not in existing_proxy_passes:
                http_block["block"].append(server_block)
                # 修改listen的端口号
                listen_node = find_nodes_by_key_value(
                    server_block, "directive", "listen"
                )[0]
                if len(listen_node["args"]) == 1:
                    listen_node["args"][0] = str(port)
                elif len(listen_node["args"]) == 2:
                    listen_node["args"] = [str(port), listen_node['args'][1]]
                else:
                    raise NotImplementedError("Not support more than 2 args")
    # 写回修改后的内容
    write_server_config(target_path, target_content)


TEMPLATE_DIR = os.path.join(
    os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
    "nginx_configs",
    "templates",
)
CFG_INIT_DIR = os.path.join(
    os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
    "nginx_configs",
    "init_confs",
)
TEMPLATE_FILE_PATH = os.path.join(
    TEMPLATE_DIR,
    "nginx.conf",
)
TEMPLATE_FILE_ADDITIONAL_SERVER_PATH = os.path.join(
    TEMPLATE_DIR,
    "additional_http_servers.conf",
)


class NginxManager:
    def __init__(self, nginx_data_dir: str):
        assert os.path.isabs(
            nginx_data_dir
        ), f"nginx_data_dir {nginx_data_dir} must be absolute path"
        # self.configs = get_config()
        self.data_dir = nginx_data_dir

        self.nginx_configs_path = os.path.join(
            self.data_dir,
            "configs",
        )
        self.nginx_conf_d_path = os.path.join(
            self.nginx_configs_path,
            "conf.d",
        )

        self.target_file_path = os.path.join(
            self.nginx_configs_path,
            "nginx.conf",
        )
        self.mime_data_path = os.path.join(
            self.nginx_configs_path,
            "mime.types",
        )
        self.shared_locations_conf_path = os.path.join(
            self.nginx_configs_path,
            "shared_locations.conf",
        )

        self.certs_path = os.path.join(
            self.nginx_configs_path,
            "certs",
        )
        self.additional_http_servers_conf_path = os.path.join(
            self.nginx_configs_path,
            "additional_http_servers.conf",
        )

        self.nginx_cache_path = os.path.join(self.data_dir, "cache")
        self.nginx_logs_path = os.path.join(self.data_dir, "logs")

        if not os.path.exists(self.nginx_cache_path):
            os.makedirs(self.nginx_cache_path, exist_ok=True)

        if not os.path.exists(self.nginx_logs_path):
            os.makedirs(self.nginx_logs_path, exist_ok=True)
        if not os.path.exists(self.nginx_conf_d_path):
            os.makedirs(self.nginx_conf_d_path, exist_ok=True)
        self.ensure_config_file(self.target_file_path)
        self.ensure_config_file(self.mime_data_path)
        self.ensure_config_file(self.shared_locations_conf_path)
        self.ensure_config_file(self.additional_http_servers_conf_path)
        self.init_nginx_configs()
        self.reload_nginx_configs()

    def init_nginx_configs(self):
        """
        Initialize the Nginx configuration by updating the listening port.
        """
        configs = get_config()
        self.copy_config_file(
            self.shared_locations_conf_path,
            overwrite=True,
            replaces={
                "{{APP_SERVER_PORT}}": str(configs["nginx_listen_port"]),
                "{{PROTOCOL}}": "https" if configs["nginx_ssl_used"] else "http",
            },
        )
        update_nginx_config(
            self.target_file_path,
            configs["nginx_listen_port"],
            configs["nginx_ssl_used"],
        )
        update_additional_http_servers(
            self.additional_http_servers_conf_path, configs["nginx_ssl_used"]
        )
        logger.info("initalized nginx configs...")

    def ensure_config_file(self, file_path: str):
        """
        Ensure that the specified configuration file exists.
        """
        self.copy_config_file(file_path, overwrite=False)

    def copy_config_file(self, file_path: str, overwrite=False, replaces=None):
        """
        Ensure that the specified configuration file exists.
        """
        if replaces is None:
            replaces = {}
        if not os.path.exists(self.nginx_configs_path):
            os.makedirs(self.nginx_configs_path, exist_ok=True)
        file_basename = os.path.basename(file_path)
        original_file_path = os.path.join(
            CFG_INIT_DIR,
            file_basename,
        )
        if (not overwrite) and (os.path.exists(file_path)):
            logger.info(f"{file_path} exists")
            return
        else:
            with open(original_file_path, "r") as f:
                content = f.read()
            for k, v in replaces.items():
                content = content.replace(k, v)
            with open(file_path, "w") as f:
                f.write(content)
            print("content", content)
            logger.info(f"Copied {original_file_path} to {file_path}")

    def reload_nginx_configs(self):
        """
        Refresh the Nginx configuration by restarting the Nginx container.
        """
        cmd = "docker stop docker-devs-nginx && docker rm docker-devs-nginx"
        os.system(cmd)
        cert_filenames = get_config()["nginx_cert_filename"]
        cert_key_path = os.path.join(self.certs_path, cert_filenames["key"])
        cert_pem_path = os.path.join(self.certs_path, cert_filenames["pem"])
        cmd = f"""docker run -itd --name docker-devs-nginx --network host \
        -v {self.nginx_cache_path}:/data/nginx/cache \
        -v {self.nginx_logs_path}:/data/nginx/logs \
    -v {self.nginx_configs_path}:/etc/nginx \
    -v {cert_key_path}:/cert/cert.key \
    -v {cert_pem_path}:/cert/cert.pem \
    {get_config()['nginx_docker_image']}"""
        # cmd = "docker stop nginx"
        print("cmd to restart docker:")
        os.system(cmd)

    def register_dev_env_proxy(
        self, dev_env_name: str, new_proxy_pass: str, reload=True
    ):
        """
        注册开发环境的代理
        """
        assert is_valid_docker_container_name(
            dev_env_name
        ), f"{dev_env_name} is not a valid name"
        add_nginx_location(
            TEMPLATE_FILE_PATH,
            self.target_file_path,
            f"/{dev_env_name}/",
            new_proxy_pass,
        )
        if reload:
            self.reload_nginx_configs()

    def register_additional_http_server(self, proxy_pass: str, reload=True) -> int:
        """
        注册额外的HTTP服务器，返回服务器的端口
        """
        free_port = get_free_port_in_allow_set(get_accessible_ports())
        if free_port is None:
            logger.fatal("No accessible port found to allocate")
            raise ValueError("No accessible port found to allocate")
        add_http_server(
            TEMPLATE_FILE_ADDITIONAL_SERVER_PATH,
            self.additional_http_servers_conf_path,
            free_port,
            proxy_pass,
        )

        if reload:
            self.reload_nginx_configs()
        return free_port

    def read_config_file(self) -> str:
        """
        Read the Nginx configuration file.
        """
        with open(self.target_file_path, "r") as file:
            return file.read()

    def read_additional_servers_file(self) -> str:
        """
        Read the Nginx additional servers file.
        """
        with open(self.additional_http_servers_conf_path, "r") as file:
            return file.read()


if __name__ == "__main__":
    reload_nginx_configs()
# add_nginx_location(
#     TEMPLATE_FILE_PATH, target_file_path, "/container-name/", "http://127.0.0.1:8000"
# )
